Skip to content

feat: form-mode cost confirmation via MCP elicitations - #367

Draft
barryroodt wants to merge 35 commits into
barryroodt/ai-1044-dual-era-servingfrom
barryroodt/mcp-elicitations-form
Draft

feat: form-mode cost confirmation via MCP elicitations#367
barryroodt wants to merge 35 commits into
barryroodt/ai-1044-dual-era-servingfrom
barryroodt/mcp-elicitations-form

Conversation

@barryroodt

@barryroodt barryroodt commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Stacked on #358. Please review that one first. This diff is against barryroodt/ai-1044-dual-era-serving at 851f01e. Merging this PR only opens a release PR. It does not ship the package.

What changed

This adds Human Confirmation for create_project and create_branch through MCP form elicitations, replacing the bypassable confirm_cost path on capable clients (AI-886).

  • mcp-utils owns the policy seam, signed Continuation State, replay protection, 120-second expiry, and handler plumbing. State keys must contain at least 32 bytes.
  • Supabase adds cost policies, stdio wiring, and the per-connection --disable-elicitations option from AI-1045. The platform PR handles disable_elicitations.
  • formDeliveryAvailable distinguishes BASE legacy clients, form-capable legacy clients, and generic clients without changing the optional Tool.formatResult contract.

The cost payload includes the live rate, a continuous-run projection, and its assumption. The prose remains PLACEHOLDER-COPY behind Design/PM sign-off, so tests pin the facts rather than the wording.

The branch is 22 commits atop #358. Earlier amended intermediates mean old review-thread SHAs may be superseded.

Behavior contracts

Clients without form support plus opted-out connections keep the existing confirm_cost behavior. The complete BASE tools/list response contains 29 tools and hashes to SHA-256 7327d077b6bdacccfa9f5853d489be6a81f2b2763ca0cc344e0bb4e8fd47371d. Exact legacy text assertions also preserve execute_sql as {result:string} and its untrusted-data boundary.

On form-capable connections, confirm_cost is hidden from discovery but remains callable with migration guidance. Decline and cancel are structured terminal variants. Expiry returns recovery guidance. Tampered state, including an edited readable exp, remains an SDK-owned -32602.

humanConfirmationEnabled === false fails protected paid tools closed while ordinary tools continue working. The gate runs before replay consumption, so an in-flight confirmation can resume with the same state inside its TTL. Generic gate telemetry uses reason: gate; serving-path telemetry preserves the capability-specific reason.

Legacy-era sessions take capabilities from the initialize handshake. Legacy-wire results project through the contextual output schema.

Verification

Final tree c5660c99c301922139a045e0c44ef49ec4223f60:

  • format:check: clean across 100 files
  • build: all three packages typecheck and build
  • mcp-utils: 69/69
  • mcp-server-supabase: 279/279 non-environmental tests, including stdio integration 21/21, Supabase elicitations 26/26, cost policies 15/15, and the legacy server suite 123/123
  • Packed platform consumer: 3/3
  • @supabase/mcp-server-postgrest: the same pre-existing 7/7 localhost :54321 failures at BASE and head

Five Anthropic-key e2e tests were excluded locally because ANTHROPIC_API_KEY was missing. The real-Claude prompt-injection e2e passed 1/1 earlier in this branch's review, before the final local review. This verification does not claim a fresh pass at the current head.

A five-lane review covered correctness/contracts, security, tests, spec compliance, and structural simplification. The initial verdict was NOT_READY; all nine synthesized findings and one post-fix telemetry issue were fixed. The final verdict is READY with no critical or important findings.

Notes for reviewers

The runtime owns minting and verification because SDK v1's createRequestStateCodec hard-reads Date.now() and does not expose typed failure causes. The implementation remains byte-compatible, with codec parity and replay boundaries covered independently. Request-state parsing, replay, codecs, and handlers now have separate responsibilities, and SDK input uses InputResponseView.

A fresh pnpm install breaks under Corepack's floating pnpm 11.16. The repo uses pnpm 10.33.2 through mise; moving the build-script policy for pnpm 11 remains a follow-up.

Please leave release.yml and the release PR out of this change. The publish-preview label records the pkg.pr.new URL for the platform stack.

Part of AI-1091

raulb and others added 30 commits August 11, 2026 13:02
Move all three published packages from the v1 monolithic
@modelcontextprotocol/sdk to the split v2 packages,
@modelcontextprotocol/server and @modelcontextprotocol/client.

BREAKING CHANGE: the peer dependency is now @modelcontextprotocol/server
instead of @modelcontextprotocol/sdk, so consumers must install the new
package. @supabase/mcp-utils also drops the exported types
ExtractRequest, ExtractNotification, and ExtractResult, and
createMcpServer now returns a bare Server rather than
Server<Request, Notification, Result>, because v2's Server class takes no
type parameters.
The hide-tools test added in #334 imports from the v1 monolithic SDK
(@modelcontextprotocol/sdk), which this branch removes. Port it to the
v2 client package and its typed callTool result, matching server.test.ts.
Assert the full sorted tools/list name set, the server identity and
declared capabilities, and one ordinary read-only list_projects call
against a loopback management API stub, so the stable SDK pin flip has
goldens to survive.

Also correct the missing-token comment: the server behavior is
unchanged, only the message the test's own client renders differs
between SDK majors.
Move the catalog from the 2.0.0-beta.3 prerelease to the released
2.0.0 line, and keep the caret range the previous
'@modelcontextprotocol/sdk': ^1.25.2 entry had. The catalog value is
substituted into every published peerDependencies entry, so an exact
pin would hand consumers an unsatisfiable peer whenever they depend on
any other 2.x of @modelcontextprotocol/server.

Also repoint the README samples, which still imported the v1 monolithic
package that this migration removes.
`ExpandRecursively` existed only to widen the intellisense of
`ExtractRequest`/`ExtractNotification`/`ExtractResult`, which this branch
removes because v2's `Server` takes no type parameters. It is still public
through `export * from './types.js'`, so leaving it would publish a type with
nothing left to widen and make its later removal a second breaking release.

Both READMEs' first example imports `@modelcontextprotocol/client`, which no
published manifest declares. Under v1 that resolved for free because
`@modelcontextprotocol/sdk` was the declared peer and npm auto-installs peers;
the v2 split moved the client into its own package, so the example no longer
runs after following the install instructions. Name it in both.

BREAKING CHANGE: `@supabase/mcp-utils` also stops exporting the type
`ExpandRecursively`.
Move the CLI's stdio entry from a one-shot server plus raw
StdioServerTransport onto the SDK's serveStdio, which owns transport
startup, connection-pinned era selection, and teardown. The default
legacy: 'serve' is the dual-era behavior we want, so no option is passed.
CLI parsing, token handling, platform construction, error output, and
exit codes are unchanged.

Export createSupabaseMcpHandler, a thin wrapper over the SDK's
createMcpHandler with legacy: 'reject'. Hosted owns authentication,
ABAC, era dispatch, logging, and request lifetime, so the package
interface carries only the strict modern entry and server construction.

The stdio wire goldens now run as an era matrix. Both eras assert the
same 29-tool set, the same server identity and capabilities, and the
same list_projects content. Modern additionally carries the protocol's
mandatory _meta['io.modelcontextprotocol/serverInfo'] stamp, which
legacy asserts is absent.

Add test:packed-platform-consumer, which packs mcp-server-supabase and
mcp-utils, installs both with plain npm into a throwaway project pinned
to the exact zod version Platform's catalog carries, then asserts the
ESM entry, the CJS entry, the type declarations, and one modern
2026-07-28 call. Registering the account tool group keeps the zod-built
tool surface under test, which an empty catalog would skip.
serveStdio routes transport startup and out-of-band wire errors only
through options.onerror, swallowing them otherwise, so without it a
startup failure was silent. The previous awaited server.connect() surfaced
it through main().catch(console.error).
Restore eager --features validation. Moving server construction into
serveStdio's lazy factory deferred parseFeatureGroups until the first valid
opening message, so an invalid --features value no longer reported and
terminated at startup. Validate the explicitly provided list before serving,
leaving platform-dependent default resolution inside createSupabaseMcpServer.
Covered by a new startup-failure test.

Make the packed-consumer gate prove what it claimed. Its only schema check was
a truthiness test on list_projects, a zero-argument tool whose healthy schema
already carries no properties, so a zod regression emitting property-less
schemas passed. Assert a parameterised witness instead, and drop skipLibCheck
so the packed declarations are really type-checked. Pin the fixture's dev
dependencies exactly and install without lifecycle scripts.

Move the fixture's source out of JS string constants into real files, collapse
the per-check pass-throughs into one runner plus a table, and drop the
esm-entry check that modern-call.mjs already covers by importing and calling
the package ESM entry. The script goes from 429 lines to 240.

Share the msw lifecycle between the two suites that had copied it, assert
status and error code rather than the SDK's exact envelope wording, guard the
integration suite against a stale dist build, and document createSupabaseMcpHandler
with the required per-request mounting pattern.
The modern validation test asserted the SDK's exact envelope message, so an
upstream rewording would fail a test whose contract held. Assert the status,
the JSON-RPC error code, and the structured envelope data instead. The
message is upstream text this package does not own; {key, problem} is
machine-readable and does not churn on rewording.
handler.close() returns a Promise that aborts in-flight exchanges, and the
example discarded it inside an event callback while also making the
createServer callback async, so a cleanup or serving failure surfaced as an
unhandled rejection. Attach a catch to both, and keep the close on res
finishing rather than on the handler resolving, since the latter would cut
streaming responses short.
Address review feedback on #358:

- Comment createSupabaseMcpHandler as modern-protocol only.
- Link the 2026-07-28 release-candidate post beside both copies of the
  protocol revision constant.
- Correct README's handler lifecycle guidance. A shared long-lived handler
  is supported when the platform is deliberately shared; the per-request
  rule applies when platform carries a per-request credential. close()
  ends the handler it is called on, so its timing follows that handler's
  lifetime rather than always being per response.
- Add a CONTRIBUTING testing section covering the suites and the scripts/
  packaging-gate pattern.
@barryroodt
barryroodt requested a review from a team as a code owner August 18, 2026 16:29
@barryroodt barryroodt added the publish-preview Runs `publish-preview` workflow to publish preview packages via https://pkg.pr.new/ label Aug 18, 2026
@barryroodt
barryroodt requested review from gregnr and raulb August 18, 2026 16:29
@pkg-pr-new

pkg-pr-new Bot commented Aug 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

pnpm add https://pkg.pr.new/@supabase/mcp-server-postgrest@c5660c9
pnpm add https://pkg.pr.new/@supabase/mcp-server-supabase@c5660c9
pnpm add https://pkg.pr.new/@supabase/mcp-utils@c5660c9

commit: c5660c9

@barryroodt barryroodt self-assigned this Aug 19, 2026
@barryroodt
barryroodt marked this pull request as draft August 19, 2026 10:00
@barryroodt
barryroodt force-pushed the barryroodt/ai-1044-dual-era-serving branch 3 times, most recently from 6a006fa to ec9ebed Compare August 20, 2026 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

publish-preview Runs `publish-preview` workflow to publish preview packages via https://pkg.pr.new/

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants